# -*- coding: utf-8 -*-
#
# Copyright 2009 Vanderbilt University
# 
# Licensed under the Apache License, Version 2.0 (the "License"); 
# you may not use this file except in compliance with the License. 
# You may obtain a copy of the License at 
# 
#     http://www.apache.org/licenses/LICENSE-2.0 
# 
# Unless required by applicable law or agreed to in writing, software 
# distributed under the License is distributed on an "AS IS" BASIS, 
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
# See the License for the specific language governing permissions and 
# limitations under the License. 

"""

.. moduleauthor:: John Paulett <john.paulett -at- vanderbilt.edu>
"""

from __future__ import division
from collections import defaultdict
from hornet.contrib.networkbuilder import NodeLookup, BuildRunner
from hornet.event import GraphsReady
from hornet.file import read_csv, join
from hornet.network import Node, create_empty_copy, rule_generation, add_edge
from hornet.plugin import Plugin, log, notify_on_finish
from hornet.util import memoize
import logging

logger = logging.getLogger(__name__)

def create_graph_from_edges(graph, edges_dict):
    for edge, size in edges_dict.iteritems():
        add_edge(graph, edge[0], edge[1], size=size)

class AbstractAbstractionProvider(object):
    @memoize
    def abstract_lookup(self):
        logger.debug('Loading attributes')
        lookup = defaultdict(list)
        for row in self.get_node_data():
            node = Node(row[0])
            lookup[node].append(row[1])
        logger.debug('Finished loading attributes')
        return dict(lookup)
    
    def get_node_data(self):
        """Returns an iterable in the format [node, node's abstracted value]
        """
        raise NotImplementedError('Must subclass %s' % self.__class__)

class FileAbstractionProvider(AbstractAbstractionProvider):
    def __init__(self, input_dir, abst_fname_fmt='attributes.csv'):
        self.input_dir = input_dir
        self.abst_fname_fmt = abst_fname_fmt
        
    def get_node_data(self):
        fn = join(self.input_dir, self.abst_fname_fmt)
        return read_csv(fn)

class DatabaseAbstractionProvider(AbstractAbstractionProvider):
    table = 'logs.sl_user_details_v'
    node_column = 'id'
    attribute_column = 'dept_descr'
    
    def __init__(self, db):
        self.db = db
    
    def get_node_data(self):
        sql = """SELECT DISTINCT a.%s, a.%s
                 FROM %s a
                 WHERE a.%s IS NOT NULL
                 """ % (self.node_column, self.attribute_column,
                        self.table,
                        self.attribute_column)
                 
        return self.db.query(sql)

#class DatabaseToFile(DatabaseAttributeProvider):
#    def __init__(self, db, input_dir):
#        self.db = db
#        fn = join(input_dir, 'attributes.csv')
#        write_csv(fn, self.get_node_attributes())
        
        
class NetworkAbstractor(object):
    """Abstracts the graph according to the abstractor function. The abstract 
    replaces the nodes by the abstract values and aggregates the results (size 
    element) for each unique abstract node.  Does not modify the input graph.  
    
    The abstractor function should take a node as input and provide some level 
    of abstraction based upon that node.  It must return a list of ids for new
    abstracted nodes (even for a single element).
    """
    def __init__(self, provider, original_graph):
        self.provider = provider
        self.original_graph = original_graph
        
        self.graph = create_empty_copy(self.original_graph) 
        self.graph.name = 'Abstract %s' % original_graph.name
        self._edges = defaultdict(float)
        self._node_lookup = NodeLookup(self.graph)
        
        self._abstract_edges()
        create_graph_from_edges(self.graph, self._edges)
        self._abstract_nodes()
        
        logger.debug('Computing rules for abstract graph %s' % self.id)
        rule_generation(self.graph)
        
        logger.debug('Finished %s' % self.id)
        
    def _abstract_edges(self):
        for edge in self.original_graph.edges_iter(data=True):
            # size the abstract_nodes method returns a list of abstracted nodes
            # we will expand all the possible combinations
            anodes0 = self._find_node_abstractions(edge[0])
            anodes1 = self._find_node_abstractions(edge[1])
            if len(anodes0) > 0:
                node_size = edge[0].size / len(anodes0)
                for n0 in anodes0:
                    n0.size += node_size
                    for n1 in anodes1:
                        ## denominator can not be zero since it is in the loop
                        edge_size = edge[2].size / (len(anodes0) * len(anodes1)) 
                        
                        #FIXME: we are assuming that the bidirectional edges 
                        #will be equal - they should be
                        ## Correction for multiple passes over this edge when subtracting
                        ## the edge size in _abstract_nodes
                        if n0 == n1:
                            edge_size /= 2
                        
                        self._edges[(n0, n1)] += edge_size
                    
    def _abstract_nodes(self):
        ## calculate the number of attributes for each abstract node
#        for node in self.original_graph.nodes_iter():
#            if node.id == "24403":
#                print 'me'
#            anodes = self._find_node_abstractions(node) ## catches nodes that are not connected by an edge
#            if len(anodes) > 0:
#                discounted_size = node.size / len(anodes)
#                for anode in anodes:
#                    anode.size += discounted_size
        
        ## avoid over counting the number of attributes for a node when
        ## those attributes are really the same attributes 
        for anode in self.graph.nodes_iter():
            if self.graph.has_edge(anode, anode):
                edge = self.graph[anode][anode] 
                anode.size -= edge.size
        
    def _find_node_abstractions(self, node):
        """Takes a node and provides the abstracted versions of the node."""
        lookup_table = self.provider.abstract_lookup()
        if node in lookup_table:
            return [self._node_lookup(id) for id in lookup_table[node]]
        else:
            ## must return a list, not a single value
            return []        

    @property
    def id(self):
        return '%s: %s' % (self.__class__, self.graph.name)
    
class NetworkAbstractionPlugin(Plugin):
    """Plugin that transforms from one level to another level. Returns
    the *provider* attribute to be set to an instance of a valid 
    implementation of :class:`hornet.plugin.abstractor.AttributeProvider`.
    """
    def __init__(self, data_provider, builder_cls=NetworkAbstractor):
        self.data_provider = data_provider
        self.builder_cls = builder_cls
        
    def accepted_events(self):
        return GraphsReady

    @notify_on_finish
    @log
    def notify(self, event):
        graphs = BuildRunner.inprocess(event.data, 
                                          data_provider=self.data_provider,
                                          builder_cls=self.builder_cls)
        return GraphsReady(graphs)

        